package com.imooc.SmartChargeStation.demo.netty;

import com.imooc.SmartChargeStation.demo.netty.handlers.*;
import com.imooc.SmartChargeStation.demo.protobuf.UserProtobuf;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.timeout.IdleStateHandler;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * author: zpy
 * description: Netty 服务端固定模板
 * date: 2025/8/20
 */

@Component
@Order(1)
/*
    这个注解的意思是Spring 会根据@Order指定的数值决定它们的执行顺序
    数值越小，优先级越高
 */
@Slf4j
public class NettyServer implements CommandLineRunner {
/*
    怎么理解？
    将其理解成一个餐厅管理系统。

    对于几个概念怎么通俗理解？
    Netty 对于NIO(主从Reactor模型) 的深度封装
    NioEventLoop : 网络指挥官
    Channel：快递小哥
    ChannelPipeline：工作流水线
    ChannelHandler: 流水线上员工
    ByteBuf：数据容器
 */

    //boss：迎宾团队，专门负责迎接客人进门
    private NioEventLoopGroup boss;
    //worker：服务员团队，负责处理客人具体服务
    private NioEventLoopGroup worker;

    private Channel channel;

    //餐厅的地址（端口号），客人只能通过换个地址找到餐厅
    @Value("${Netty.server.port}")
    private int port;

//    public NettyServer(int port) {
//        this.port = port;
//    }


//    @PostConstruct
//    public void init() {
//        start();
//    }

    /**
     * description: Netty启动（餐厅开业准备）
     */

    public void start() {

        //处理网络请求
        boss = new NioEventLoopGroup();
        //处理网络IO
        worker = new NioEventLoopGroup();

        //bootstra理解成餐厅经理，是整个流程的“总调度”，是一条主线将Netty的主要组件串联起来
        ServerBootstrap bootstra = new ServerBootstrap();
        bootstra
                //负责安排迎宾团队（boss）和服务团队（worker）的分工-配置NioEventLoop
                .group(boss,worker)

                //确定餐厅的 "大门类型"是玻璃门还是旋转门，决定了如何接收客人-配置Channel
                .channel(NioServerSocketChannel.class)

                //制定排队规则规定门口等待区的大小，就像规定门口最多能排多少人-设置指定大小的接收缓冲区（TCP）
                .option(ChannelOption.SO_RCVBUF, 3)

                //安排 "服务流水线"规定客人进店后要经过哪些服务环节-将ChannelHandler添加上ChannelPipeline
                .childHandler(new ChannelInitializer<>() {
                    //Channel初始化
                    @Override
                    protected void initChannel(Channel channel) throws Exception {

                        //Channel初始化是伴随ChannelPipeline的初始化,取出ChannelPipeline
                        ChannelPipeline pipeline = channel.pipeline();

                        //方案1：分隔符方式
                        //定义分隔符，在pipeline上写下面的代码
                        //ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
                        pipeline
/*服务端监听客户端是否掉线*/
                                //发送心跳包
                                .addLast(new IdleStateHandler(60,60,60, TimeUnit.SECONDS))
                                //自定义的超时处理逻辑，IdleStateHandler只是做空闲状态监测，具体的业务逻辑需要另外实现，nit: 时间单位
                                .addLast(new ImoocServerHeartBeatHandler())
/*
    怎么理解？

    服务员定时查看客人状态（IdleStateHandler 空闲状态监测）：
    1.如果60秒没有看见客人动筷子（读空闲），可能离座了，考虑是否要清理桌子
    readerIdleTime:读操作空闲时间(读超时) 在指定的时间间隔没有在Channel读到数据， 会触发名叫READ_IDLE叫做IdleStateEvent事件
    2.如果60秒客人没有要求服务员上菜（写空闲），服务员主动上去询问是否需要服务点菜
    writerIdleTime: 写操作空闲时间(写超时) 在指定的时间间隔没有在Channel写入数据， 会触发名叫WRITE_IDLE叫做IdleStateEvent事件
    3.如果60秒内客人既没动筷子也没点菜（读写空闲），会换一个服务员为他服务
    allIdleTime: 读写操作空闲时间(读/写超时) 在指定的时间间隔没有在Channel读写数据， 会触发名叫ALL_IDLE叫做IdleStateEvent事件
    此时，IdleStateHandler会调用fireUserEventTriggered(evt)，将IdleStateEvent事件传递到下一个Handler
*/


/*
    粘包半包问题：
    1.什么是粘包半包？
    粘包：多个独立的数据包被粘在一起发送，接收方无法区分数据包边界。
    例如：发送方发送了三个数据包"ABC"、"DEF"、"GHI"，接收方可能一次性收到"ABCDEFGHI"。
    半包：一个完整的数据包被拆分成多个部分发送，接收方需要拼接才能得到完整数据。
    例如：发送方发送了数据包"ABCDEFGHI"，接收方可能收到"ABC"、"DEF"、"GHI"。

    2.如何理解三种粘包半包的解决方案？（以粘包为例）
    a.分隔符方式（DelimiterBasedFrameDecoder）
    发送时给 "ABC"、"DEF"、"GHI" 分别添加分隔符|，通过TCP传输变为 "ABC|DEF|GHI|"，接收方按 "|" 拆分即可得到三个独立包。
    b.固定长度方式（FixedLengthFrameDecoder）
    因 "ABC"、"DEF"、"GHI" 均为 3 字节，接收方收到合并的 "ABCDEFGHI" 后，每次读取 3 字节，直接拆分出三个包。
    c.使用Protobuf编解码（ProtobufVarint32FrameDecoder + ProtobufDecoder）
    发送 "ABC"、"DEF"、"GHI" 时，分别添加长度前缀 "3"，组合为 "[3][ABC][3][DEF][3][GHI]"，接收方先读 "3" 再读 3 字节得 "ABC"，依次拆分所有包。

    3.相关解决方案的代码是什么？
    方案1：分隔符方式
    //定义分隔符，在pipeline上写下面的代码
    //ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
    //在pipeline下写下面的代码：
    // 分隔符帧解码器，最大长度1024
    .addLast(new DelimiterBasedFrameDecoder(1024, delimiter))
    // 字符串解码器，将ByteBuf转为String
    .addLast(new StringDecoder())

    方案2:固定长度方式
    //在pipeline下写下面的代码：
    // 固定长度帧解码器，长度11
    .addLast(new FixedLengthFrameDecoder(11))
    // 字符串解码器
    .addLast(new StringDecoder())
    // 可选择添加ChannelInboundHandlerAdapter处理器
    .addLast(new ImoocServerHandlerAdapter())
    // 或添加SimpleChannelInboundHandler处理器
    .addLast(new ImoocServerHandler());

    方案3: Protobuf序列：
    //在接收字节流时，先解析出开头的 “长度前缀”，再根据长度截取完整的 Protobuf 消息字节流。（解码器）
    .addLast(new ProtobufVarint32FrameDecoder() )
    // Protobuf解码器，指定解码目标类型
    .addLast(new ProtobufDecoder(UserProtobuf.User.getDefaultInstance()))
    // Protobuf消息处理器
    .addLast(new ImoocServerProtobufHandler());
 */
                                //方案1：分隔符方式
                                // 分隔符帧解码器，最大长度1024
                                //.addLast(new DelimiterBasedFrameDecoder(1024, delimiter))
                                // 字符串解码器，将ByteBuf转为String
                                //.addLast(new StringDecoder())

                                //方案2：固定长度方式
                                // 固定长度帧解码器，长度11
                                //.addLast(new FixedLengthFrameDecoder(8))

                                //方案3：Protobuf序列
                                //在接收字节流时，先解析出开头的 “长度前缀”，再根据长度截取完整的 Protobuf 消息字节流。
                                //.addLast(new ProtobufVarint32FrameDecoder() )
                                //Protobuf解码器
                                //.addLast(new ProtobufDecoder(UserProtobuf.User.getDefaultInstance()))
                                //Protobuf消息实例接收
                                //.addLast(new ImoocServerProtobufHandler())

                                //添加字符串解码器
                                //.addLast(new StringDecoder())
                                //粘包半包问题场景复现服务端端处理器
                                //.addLast(new ImoocServerPkgHandler())

                                //两种不同的入站处理器，均用于处理客户端连接后的入站事件
                                //方式1：直接实现 ChannelInboundHandler 接口，需显式实现所有方法
                                //.addLast(new ImoocServerHandler())
                                //方式2：继承适配器类，仅重写所需方法
                                .addLast(new ImoocServerHandlerAdapter())


//测试protocolbuf编解码器服务端代码：
                          //添加Protobuf解码器
                         //.addLast(new ProtobufDecoder(UserProtobuf.User.getDefaultInstance()))
                         //添加Protobuf消息实例接收器
                         //.addLast(new ImoocServerProtobufHandler())
                        ;
                    }
                });

        //Netty所有操作都是异步的，会返回Future对象，可以利用这个对象可以实现异步操作的通知
        //绑定服务器直到绑定成功为止
        ChannelFuture future = null;
        try {

            //bind(port).sync()阻塞是短暂的，在Netty服务器端初始化完成之后，这个阻塞方法就完成了
            //餐厅在经历选址装修完成之后，.sync()方法会阻塞，相当于等待餐厅的开业仪式完成，但这个等待时间很短
            //流程：将Netty服务端的线程设置wait状态，SpringBoot的主线程不会进入到finally执行Netty服务端关闭的代码
            future = bootstra.bind(port).sync();
            log.info(">>>>>Netty 服务器监听的端口：{}",port);
            if(future.isSuccess()) {
                log.info(">>>>>Netty 服务器启动成功");
            }
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {

            log.info(">>>>>Netty服务端关闭....");
            //优雅关闭组件
            destory();

        }

    }

    /**
     * description: Netty 关闭
     */
    @PreDestroy
/*
    该注解作用是对象销毁之前会执行@PreDestroy所修饰的方法
 */
    public void destory() {

        try {
            if(boss != null)
                boss.shutdownGracefully().sync();
            if(worker != null)
                worker.shutdownGracefully().sync();
            if(channel != null)
                channel.closeFuture().syncUninterruptibly();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * description: Netty Server以另一个线程启动
     */
    @Async
    @Override
    public void run(String... args) throws Exception {
        start();
    }
}
